/*
    Copyright 2009 Barnet Wagman

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
 */

package ot.fs;

import java.util.*;
import ot.*;
import java.io.*;

/** An {@link Ot} that stores objects in the local file system.
 *
 * @author Barnet Wagman
 */
public class OtFs extends AbstractOt implements Ot, Serializable {

    static final long serialVersionUID = ConstantsOt.UID_VERSION;

    String fileSep;

    public ContextFs context;


        /** This constuctor creates a list of serializers that contains
         *  {@link StandardSerializer} and {@link GzipSerializer}.  The
         *  default serializer is set to {@link GzipSerializer}.
         *  <tt>cacheSize=</tt>{@link ConstantsOt#MAX_OBJ_IN_CACHE},
         *  <tt>cacheDecrement=</tt>{@link ConstantsOt#CACHE_DECREMENT}.
         *
         */
    public OtFs(ContextFs context) throws NoSuchContextException {
        super();
        this.context = context;
        fileSep = System.getProperty("file.separator");
        if ( !context.dir.exists() )
            throw new NoSuchContextException("Directory " +
                    context.dir.getPath() + " does not exist.");
    }

        /** This constuctor creates a list of serializers that contains
         *  {@link StandardSerializer} and {@link GzipSerializer}.  The
         *  default serializer is set to {@link GzipSerializer}.
         *
         * @param context
         * @param cacheSize
         * @param cacheDecrement
         * @throws ot.NoSuchContextException
         */
    public OtFs(ContextFs context, int cacheSize, int cacheDecrement)
        throws NoSuchContextException {

        super(cacheSize, cacheDecrement);
        this.context = context;
        fileSep = System.getProperty("file.separator");
        if ( !context.dir.exists() )
            throw new NoSuchContextException("Directory " +
                    context.dir.getPath() + " does not exist.");
    }

        /** This constuctor creates a list of serializers that contains
         *  {@link StandardSerializer}, {@link GzipSerializer}, and the
         *  default serializer (if it's not the standard or gzip).
         *  <tt>cacheSize=</tt>{@link ConstantsOt#MAX_OBJ_IN_CACHE},
         *  <tt>cacheDecrement=</tt>{@link ConstantsOt#CACHE_DECREMENT}.
         * 
         * @param context
         * @param serializer
         * @throws ot.NoSuchContextException
         */
    public OtFs(ContextFs context,Serializer serializer)
         throws NoSuchContextException {

        super(serializer);
        this.context = context;
        fileSep = System.getProperty("file.separator");
        if ( !context.dir.exists() )
            throw new NoSuchContextException("Directory " +
                    context.dir.getPath() + " does not exist.");
    }

    public void setContext(Context context) 
        throws OtException, NoSuchContextException {
        if ( !(context instanceof ContextFs) )
            throw new OtException("OtFs requires " +
                ContextFs.class.getName() + ", not " +
                ((context != null)?context.getClass().getName():"null"));
        if ( !((ContextFs) context).dir.exists() )
            throw new NoSuchContextException("Directory " +
                    ((ContextFs) context).dir.getPath() + " does not exist.");
    }

        /** {@inheritDoc}
         * 
         * @return
         */
    public Context getContext() {
        return(context);
    }

        /** {@inheritDoc}
         *
         * @param path
         * @return
         * @throws ot.OtException
         */
    public boolean has(OPath path) throws OtException {
        File f = locToFile(new LocO(context,path));       
        return( f.exists() && !f.isDirectory() );
    }

       /** {@inheritDoc}
         *
         * @param path
         * @param obj
         */
    public void add(OPath path, Serializable obj)
        throws OtException {
        try {
            add(path,obj,true);
        }
        catch(ObjectExistsException ox) {}
    }

         /** {@inheritDoc}
          *
          * @param path
          * @param obj
          * @param replaceExtant if <tt>true</tt>, an existing object will be
          *     <tt>path</tt> is replace. If <tt>false</tt> and
          *     there is an existing object with <tt>path</tt>,
          *     an {@link ObjectExistsException} is thrown.
          *
          * @throws ot.OtException
          * @throws ot.ObjectExistsException if <tt>replaceExtant == false</tt>
          *     and an objet with the specified path already exists.
          */
    public void add(OPath path, Serializable obj, boolean replaceExtant)
        throws ObjectExistsException, OtException {
        
        File f = locToFile(new LocO(context,path));
        if ( f.exists() ) {
            if ( replaceExtant ) f.delete();
            else throw new ObjectExistsException(f.getPath() +
                                                 " already exists.");
        }
        if ( !f.getParentFile().exists() ) f.getParentFile().mkdirs();

        getSer(path).objectToFile(f, obj);
    }

    /** {@inheritDoc}
     *
     * @param path
     * @return the object with <tt>path</tt> or <tt>null</tt> if no
     *      such object exists.
     * @throws ot.OtException
     */
    public Serializable get(OPath path) throws OtException {
        
        LocO loc = new LocO(context,path);
        Serializable x = readCache.get(loc);
        if ( x != null ) return(x);
        else {
            File f = locToFile(loc);
            if ( f.exists() && !f.isDirectory() ) {
                x = (Serializable) getSer(path).fileToObject(f);
                readCache.add(loc,x);
                return(x);
            }
            else if ( !f.exists() ) return(null);
            else throw new OtException(f.getPath() + " is a directory.");
        }
    }

        /** {@inheritDoc}
         *
         * @param loc
         * @return
         * @throws ot.OtException
         */
    public Serializable get(LocO loc) throws OtException {
        
        Serializable x = readCache.get(loc);
        if ( x != null ) return(x);
        else {
            File f = locToFile(loc);
            if ( f.exists() && !f.isDirectory() ) {
                x = (Serializable) getSer(loc).fileToObject(f);
                readCache.add(loc,x);
                return(x);
            }
            else if ( !f.exists() ) return(null);
            else throw new OtException(f.getPath() + " is a directory.");
        }
    }

        /** {@inheritDoc}
         *
         *  @param locs only <tt>LocO</tt>s are used to retrieve
         *      objects.  <tt>LocD</tt>s are ignored.
         *  @return a <tt>List</tt> of objects or an empty list if no
         *      objects matching <tt>locs</tt> are found.
         */
    public List<Serializable> get(List<Loc> locs) throws OtException {

        List<Serializable> objs = new ArrayList<Serializable>();

        for ( Loc loc : locs ) {
            if ( loc instanceof LocO ) {
                objs.add(get((LocO) loc));
            }
        }
        return(objs);
    }

        /** <tt>OtFs</tt> does not use a cache, so this method is
         *  identical to {@link #get(ot.OPath) }.
         * @param path
         * @return
         */
    public Serializable getCached(OPath path) {
        LocO loc = new LocO(context,path);
        return(readCache.get(loc));
    }

        /** {@inheritDoc}
         *
         * @param path
         * @throws ot.OtException
         */
    public void delete(OPath path) throws OtException {
        File f = locToFile(new LocO(context,path));
        if ( f.exists() ) {
            if ( !f.isDirectory() ) {
                f.delete();
                File parent = f.getParentFile();
                if ( (parent != null) && parent.exists() &&
                     parent.isDirectory() ) {
                    if ( parent.listFiles().length == 0 )
                        parent.delete();
                }

            }
            else throw new OtException(f.getPath() + " is a directory; " +
                                            " NOT deleted.");
        }
        else throw new OtException(f.getPath() + " does not exist.");
    }


    private void deleteDirRecursively(File dir) {

        File[] fs = dir.listFiles();
        for ( File f : fs ) {
            if ( !f.isDirectory() ) f.delete();
            else deleteDirRecursively(f);
        }
        dir.delete();
    }

        /** {@inheritDoc}
          *
          * @param path
          * @return
          * @throws ot.OtException
          */
    public List<Loc> list(DPath path) throws OtException {

        LocD dir = new LocD(context,path);
        File d = locToFile(dir);
        if ( !d.exists() )
            throw new OtException(d.getPath() + " does not exist.");
        if ( !d.isDirectory() )
            throw new OtException(d.getPath() + " is not a directory.");

        List<Loc> locs = new ArrayList<Loc>();

        File[] fs = d.listFiles();
        for ( File f : fs ) {
            if ( f.isDirectory() ) 
                locs.add(createLocD(((ContextFs) dir.context),f));
            else locs.add(createLocO(((ContextFs) dir.context),f));
        }
        return(locs);
    }


        /** {@inheritDoc}
         *       
         * @return
         */
    public List<LocO> listContext() throws OtException {
        return( listObjects(context.dir) );
    }

    private List<LocO> listObjects(File dir) {

        List<LocO> locos = new ArrayList<LocO>();
        File[] fs = dir.listFiles();

        for ( File f : fs ) {
            if ( f.isDirectory() ) {
                locos.addAll(listObjects(f));
            }
            else locos.add(createLocO(context,f));
        }
        return(locos);
    }

        /** {@inheritDoc}
         *
         * @param path
         * @return
         * @throws ot.OtException
         */
    public List<Loc> listTree(DPath path) throws OtException {

        List<Loc> treeList = new ArrayList<Loc>();

        List<Loc> locs = list(path);

        treeList.addAll(locs);

        for ( Loc loc : locs ) {
            if ( loc instanceof LocD ) {
                treeList.addAll(listTree((DPath) loc.getPath()));
            }
        }
        return(treeList);
    }
    
        // ------------------------------------------

        /** Creates path string <i>without</i> a leading file separator.
         *
         * @param path
         * @return path string <i>without</i> a leading file separator or
         *          null if the path is empty.
         */
    public String pathToString(Path path) {
       
        if ( (path.pathnames == null) || (path.pathnames.length < 1) )
            return(null);

          StringBuilder s = new StringBuilder(path.pathnames[0]);

          for ( int i = 1; i < path.pathnames.length; i++ ) {
              s.append(fileSep + path.pathnames[i]);
          }
          return(s.toString());
    }

        /** Creates a <tt>File</tt> from a {@link Loc} in this <tt>OtFs</tt>'s
         *  context.
         *
         * @param loc
         * @return
         * @throws ot.OtException
         */
    public File locToFile(Loc loc) throws OtException {

        if ( !(loc.context instanceof ContextFs) ) {
            throw new OtException("Loc '" + loc + "' has a " +
                "non-fs Context");
        }

        return( new File( ((ContextFs) loc.context).dir,
                          pathToString(loc.getPath()) ) );
    }

        // ------------------------------------------------------

    private LocD createLocD(ContextFs context,File dir) {
       
        String pathInContext = 
                stripPrefix(dir.getPath(),context.dir.getPath() +"/");
        return(new LocD(context,createDPath(pathInContext)));
    }

    private LocO createLocO(ContextFs context,File dir) {

        String pathInContext =
                stripPrefix(dir.getPath(),context.dir.getPath() + "/");
        return(new LocO(context,createOPath(pathInContext)));
    }

    private DPath createDPath(String pathString) {
        return( new DPath(pathString.split(fileSep)) );
    }

    private OPath createOPath(String pathString) {
        return( new OPath(pathString.split(fileSep)) );
    }

    private String stripPrefix(String s, String prefix) {
        return( s.substring(prefix.length()) );
    }
}
